//CHECKSTYLE:OFF
package org.hibernate.engine.transaction.internal;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.sql.Connection;
import java.util.ArrayList;
import java.util.List;

import org.jboss.logging.Logger;

import org.hibernate.ConnectionReleaseMode;
import org.hibernate.ResourceClosedException;
import org.hibernate.engine.jdbc.internal.JdbcCoordinatorImpl;
import org.hibernate.engine.jdbc.spi.JdbcCoordinator;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.engine.transaction.internal.jta.JtaStatusHelper;
import org.hibernate.engine.transaction.spi.JoinStatus;
import org.hibernate.engine.transaction.spi.SynchronizationRegistry;
import org.hibernate.engine.transaction.spi.TransactionContext;
import org.hibernate.engine.transaction.spi.TransactionCoordinator;
import org.hibernate.engine.transaction.spi.TransactionEnvironment;
import org.hibernate.engine.transaction.spi.TransactionFactory;
import org.hibernate.engine.transaction.spi.TransactionImplementor;
import org.hibernate.engine.transaction.spi.TransactionObserver;
import org.hibernate.engine.transaction.synchronization.internal.RegisteredSynchronization;
import org.hibernate.engine.transaction.synchronization.internal.SynchronizationCallbackCoordinatorImpl;
import org.hibernate.engine.transaction.synchronization.spi.SynchronizationCallbackCoordinator;
import org.hibernate.internal.CoreMessageLogger;
import org.hibernate.internal.util.collections.CollectionHelper;
import org.hibernate.service.jta.platform.spi.JtaPlatform;

/**
 * Standard implementation of the Hibernate {@link TransactionCoordinator}
 * <p/>
 * IMPL NOTE : Custom serialization handling!
 * @author Steve Ebersole
 */
public class TransactionCoordinatorImpl implements TransactionCoordinator {

    /**
     * To be completed.
     */
    private static final CoreMessageLogger LOG = Logger
            .getMessageLogger(CoreMessageLogger.class,
                    TransactionCoordinatorImpl.class.getName());

    /**
     * To be completed.
     */
    private final transient TransactionContext transactionContext;

    /**
     * To be completed.
     */
    private final transient JdbcCoordinatorImpl jdbcCoordinator;

    /**
     * To be completed.
     */
    private final transient
    TransactionFactory<TransactionImplementor> transactionFactory;

    /**
     * To be completed.
     */
    private final transient TransactionEnvironment transactionEnvironment;

    /**
     * To be completed.
     */
    private final transient List<TransactionObserver> observers;

    /**
     * To be completed.
     */
    private final transient SynchronizationRegistryImpl synchronizationRegistry;

    /**
     * To be completed.
     */
    private transient TransactionImplementor currentHibernateTransaction;

    /**
     * To be completed.
     */
    private transient
    SynchronizationCallbackCoordinatorImpl callbackCoordinator;

    /**
     * To be completed.
     */
    private transient boolean open = true;

    /**
     * To be completed.
     */
    private transient boolean synchronizationRegistered;

    /**
     * To be completed.
     */
    private transient boolean ownershipTaken;

    /**
     * To be completed.
     */
    public TransactionCoordinatorImpl(Connection userSuppliedConnection,
            TransactionContext transactionContext) {
        this.transactionContext = transactionContext;
        this.jdbcCoordinator = new JdbcCoordinatorImpl(userSuppliedConnection,
                this);
        this.transactionEnvironment = transactionContext
                .getTransactionEnvironment();
        this.transactionFactory = this.transactionEnvironment
                .getTransactionFactory();
        this.observers = new ArrayList<TransactionObserver>();
        this.synchronizationRegistry = new SynchronizationRegistryImpl();
        reset();

        final boolean registerSynchronization = transactionContext
                .isAutoCloseSessionEnabled()
                || transactionContext.isFlushBeforeCompletionEnabled()
                || transactionContext.getConnectionReleaseMode() == ConnectionReleaseMode.AFTER_TRANSACTION;
        if (registerSynchronization) {
            pulse();
        }
    }

    public TransactionCoordinatorImpl(TransactionContext transactionContext,
            JdbcCoordinatorImpl jdbcCoordinator,
            List<TransactionObserver> observers) {
        this.transactionContext = transactionContext;
        this.jdbcCoordinator = jdbcCoordinator;
        this.transactionEnvironment = transactionContext
                .getTransactionEnvironment();
        this.transactionFactory = this.transactionEnvironment
                .getTransactionFactory();
        this.observers = observers;
        this.synchronizationRegistry = new SynchronizationRegistryImpl();
        reset();
    }

    /**
     * Reset the internal state.
     */
    public void reset() {
        synchronizationRegistered = false;
        ownershipTaken = false;

        if (currentHibernateTransaction != null) {
            currentHibernateTransaction.invalidate();
        }
        currentHibernateTransaction = transactionFactory().createTransaction(
                this);
        if (transactionContext.shouldAutoJoinTransaction()) {
            currentHibernateTransaction.markForJoin();
            currentHibernateTransaction.join();
        }

        // IMPL NOTE : reset clears synchronizations (following jta spec), but
        // not observers!
        synchronizationRegistry.clearSynchronizations();
    }

    public void afterTransaction(TransactionImplementor hibernateTransaction,
            int status) {
        LOG.trace("after transaction completion");

        final boolean success = JtaStatusHelper.isCommitted(status);

        if (sessionFactory().getStatistics().isStatisticsEnabled()) {
            transactionEnvironment.getStatisticsImplementor().endTransaction(
                    success);
        }

        getJdbcCoordinator().afterTransaction();

        getTransactionContext().afterTransactionCompletion(
                hibernateTransaction, success);
        sendAfterTransactionCompletionNotifications(hibernateTransaction,
                status);
        reset();
    }

    private SessionFactoryImplementor sessionFactory() {
        return transactionEnvironment.getSessionFactory();
    }

    public boolean isSynchronizationRegistered() {
        return synchronizationRegistered;
    }

    @Override
    public boolean isTransactionInProgress() {
        return getTransaction().isActive()
                && getTransaction().getJoinStatus() == JoinStatus.JOINED;
    }

    @Override
    public TransactionContext getTransactionContext() {
        return transactionContext;
    }

    @Override
    public JdbcCoordinator getJdbcCoordinator() {
        return jdbcCoordinator;
    }

    private TransactionFactory<TransactionImplementor> transactionFactory() {
        return transactionFactory;
    }

    private TransactionEnvironment getTransactionEnvironment() {
        return transactionEnvironment;
    }

    @Override
    public TransactionImplementor getTransaction() {
        if (!open) {
            throw new ResourceClosedException(
                    "This TransactionCoordinator has been closed");
        }
        pulse();
        return currentHibernateTransaction;
    }

    public void afterNonTransactionalQuery(boolean success) {
        // check to see if the connection is in auto-commit mode (no connection
        // means aggressive connection
        // release outside a JTA transaction context, so MUST be autocommit
        // mode)
        boolean isAutocommit = getJdbcCoordinator().getLogicalConnection()
                .isAutoCommit();
        getJdbcCoordinator().getLogicalConnection().afterTransaction();

        if (isAutocommit) {
            for (TransactionObserver observer : observers) {
                observer.afterCompletion(success, this.getTransaction());
            }
        }
    }

    @Override
    public void resetJoinStatus() {
        getTransaction().resetJoinStatus();
    }

    @SuppressWarnings({"unchecked"})
    private void attemptToRegisterJtaSync() {
        if (synchronizationRegistered) {
            return;
        }

        // Has the local transaction (Hibernate facade) taken on the
        // responsibility of driving the transaction inflow?
        if (currentHibernateTransaction.isInitiator()) {
            return;
        }

        if (!transactionContext.shouldAutoJoinTransaction()) {
            if (currentHibernateTransaction.getJoinStatus() != JoinStatus.MARKED_FOR_JOINED) {
                LOG.debug("Skipping JTA sync registration due to auto join checking");
                return;
            }
        }

        // IMPL NOTE : At this point the local callback is the "maybe" one. The
        // only time that needs to change is if
        // we are able to successfully register the transaction synchronization
        // in which case the local callback would become
        // non driving. To that end, the following checks are simply opt outs
        // where we are unable to register the
        // synchronization

        JtaPlatform jtaPlatform = getTransactionEnvironment().getJtaPlatform();
        if (jtaPlatform == null) {
            // if no jta platform was registered we wont be able to register a
            // jta synchronization
            return;
        }

        // Can we resister a synchronization
        if (!jtaPlatform.canRegisterSynchronization()) {
            LOG.trace("registered JTA platform says we cannot currently resister synchronization; skipping");
            return;
        }

        // Should we resister a synchronization
        if (!transactionFactory().isJoinableJtaTransaction(this,
                currentHibernateTransaction)) {
            LOG.trace("TransactionFactory reported no JTA transaction to join; skipping Synchronization registration");
            return;
        }

        jtaPlatform.registerSynchronization(new RegisteredSynchronization(
                getSynchronizationCallbackCoordinator()));
        synchronizationRegistered = true;
        LOG.debug("successfully registered Synchronization");
    }

    @Override
    public SynchronizationCallbackCoordinator getSynchronizationCallbackCoordinator() {
        if (callbackCoordinator == null) {
            callbackCoordinator = new SynchronizationCallbackCoordinatorImpl(
                    this);
        }
        return callbackCoordinator;
    }

    public void pulse() {
        if (transactionFactory().compatibleWithJtaSynchronization()) {
            // the configured transaction strategy says it supports callbacks
            // via JTA synchronization, so attempt to
            // register JTA synchronization if possible
            attemptToRegisterJtaSync();
        }
    }

    public Connection close() {
        open = false;
        // XXX: we won't call reset here
        // reset();
        observers.clear();
        return jdbcCoordinator.close();
    }

    public SynchronizationRegistry getSynchronizationRegistry() {
        return synchronizationRegistry;
    }

    public void addObserver(TransactionObserver observer) {
        observers.add(observer);
    }

    @Override
    public void removeObserver(TransactionObserver observer) {
        observers.remove(observer);
    }

    @Override
    public boolean isTransactionJoinable() {
        return transactionFactory().isJoinableJtaTransaction(this,
                currentHibernateTransaction);
    }

    @Override
    public boolean isTransactionJoined() {
        return currentHibernateTransaction != null
                && currentHibernateTransaction.getJoinStatus() == JoinStatus.JOINED;
    }

    public void setRollbackOnly() {
        getTransaction().markRollbackOnly();
    }

    @Override
    public boolean takeOwnership() {
        if (ownershipTaken) {
            return false;
        } else {
            ownershipTaken = true;
            return true;
        }
    }

    @Override
    public void sendAfterTransactionBeginNotifications(
            TransactionImplementor hibernateTransaction) {
        for (TransactionObserver observer : observers) {
            observer.afterBegin(currentHibernateTransaction);
        }
    }

    @Override
    public void sendBeforeTransactionCompletionNotifications(
            TransactionImplementor hibernateTransaction) {
        synchronizationRegistry
                .notifySynchronizationsBeforeTransactionCompletion();
        for (TransactionObserver observer : observers) {
            observer.beforeCompletion(hibernateTransaction);
        }
    }

    @Override
    public void sendAfterTransactionCompletionNotifications(
            TransactionImplementor hibernateTransaction, int status) {
        final boolean successful = JtaStatusHelper.isCommitted(status);
        for (TransactionObserver observer : observers) {
            observer.afterCompletion(successful, hibernateTransaction);
        }
        synchronizationRegistry
                .notifySynchronizationsAfterTransactionCompletion(status);
    }

    // serialization
    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    /**
     * To be completed.
     */
    public void serialize(final ObjectOutputStream oos) throws IOException {
        jdbcCoordinator.serialize(oos);
        oos.writeInt(observers.size());
        for (TransactionObserver observer : observers) {
            oos.writeObject(observer);
        }
    }

    /**
     * To be completed.
     */
    public static TransactionCoordinatorImpl deserialize(
            final ObjectInputStream ois,
            final TransactionContext transactionContext)
            throws ClassNotFoundException, IOException {
        final JdbcCoordinatorImpl jdbcCoordinator = JdbcCoordinatorImpl
                .deserialize(ois, transactionContext);
        final int observerCount = ois.readInt();
        final List<TransactionObserver> observers = CollectionHelper
                .arrayList(observerCount);
        for (int i = 0; i < observerCount; i++) {
            observers.add((TransactionObserver) ois.readObject());
        }
        final TransactionCoordinatorImpl transactionCoordinator = new TransactionCoordinatorImpl(
                transactionContext, jdbcCoordinator, observers);
        jdbcCoordinator.afterDeserialize(transactionCoordinator);
        return transactionCoordinator;
    }

}
